/**
 * ***************************************************************************** Copyright (c) 2004,
 * 2006 Subclipse project and others. All rights reserved. This program and the accompanying
 * materials are made available under the terms of the Eclipse Public License v1.0 which accompanies
 * this distribution, and is available at http://www.eclipse.org/legal/epl-v10.html
 *
 * <p>Contributors: Subclipse project committers - initial API and implementation
 * ****************************************************************************
 */
package org.tigris.subversion.subclipse.ui.operations;

import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.team.ui.TeamOperation;
import org.eclipse.ui.IWorkbenchPart;
import org.tigris.subversion.subclipse.core.SVNException;
import org.tigris.subversion.subclipse.core.SVNStatus;
import org.tigris.subversion.subclipse.core.util.Assert;
import org.tigris.subversion.subclipse.ui.ISVNUIConstants;
import org.tigris.subversion.subclipse.ui.Policy;
import org.tigris.subversion.subclipse.ui.SVNUIPlugin;

/**
 * This class is the abstract superclass for SVN operations. It provides error handling, prompting
 * and other UI.
 */
public abstract class SVNOperation extends TeamOperation {

  private int statusCount;

  private boolean involvesMultipleResources = false;

  private List errors = new ArrayList(); // of IStatus

  protected static final IStatus OK = Status.OK_STATUS; // $NON-NLS-1$

  private Shell shell;

  // instance variable used to indicate behavior while prompting for overwrite
  private boolean confirmOverwrite = true;

  protected SVNOperation(IWorkbenchPart part) {
    super(part);
  }

  /* (non-Javadoc)
   * @see org.eclipse.team.internal.ui.actions.TeamOperation#getJobName()
   */
  protected String getJobName() {
    return getTaskName();
  }

  /* (non-Javadoc)
   * @see org.eclipse.team.ui.TeamOperation#getOperationIcon()
   */
  protected URL getOperationIcon() {
    try {
      URL baseURL = SVNUIPlugin.getPlugin().getBundle().getEntry("/"); // $NON-NLS-1$
      return new URL(baseURL, ISVNUIConstants.ICON_PATH + ISVNUIConstants.IMG_CHECKOUT);
    } catch (MalformedURLException e) {
      return null;
    }
  }

  /* (non-Javadoc)
   * @see org.eclipse.jface.operation.IRunnableWithProgress#run(org.eclipse.core.runtime.IProgressMonitor)
   */
  public final void run(IProgressMonitor monitor)
      throws InvocationTargetException, InterruptedException {
    startOperation();
    try {
      monitor = Policy.monitorFor(monitor);
      monitor.beginTask(null, 100);
      monitor.setTaskName(getTaskName());
      execute(Policy.subMonitorFor(monitor, 100));
      endOperation();
    } catch (SVNException e) {
      // TODO: errors may not be empty (i.e. endOperation has not been executed)
      if (!e.operationInterrupted()) {
        throw new InvocationTargetException(e);
      }
    } finally {
      monitor.done();
    }
  }

  protected void startOperation() {
    statusCount = 0;
    resetErrors();
    confirmOverwrite = true;
  }

  protected void endOperation() throws SVNException {
    handleErrors((IStatus[]) errors.toArray(new IStatus[errors.size()]));
  }

  /**
   * Subclasses must override this method to perform the operation. Clients should never call this
   * method directly.
   *
   * @param monitor
   * @throws SVNException
   * @throws InterruptedException
   */
  protected abstract void execute(IProgressMonitor monitor)
      throws SVNException, InterruptedException;

  protected void addError(IStatus status) {
    if (status.isOK()) return;
    if (isLastError(status)) return;
    errors.add(status);
  }

  protected void collectStatus(IStatus status) {
    if (isLastError(status)) return;
    statusCount++;
    if (!status.isOK()) addError(status);
  }

  protected void resetErrors() {
    errors.clear();
    statusCount = 0;
  }

  /**
   * Get the last error taht occured. This can be useful when a method has a return type but wants
   * to signal an error. The method in question can add the error using <code>addError(IStatus)
   * </code> and return null. The caller can then query the error using this method. Also, <code>
   * addError(IStatus)</code> will not add the error if it is already on the end of the list (using
   * identity comparison) which allows the caller to still perform a <code>collectStatus(IStatus)
   * </code> to get a valid operation count.
   *
   * @return
   */
  protected IStatus getLastError() {
    Assert.isTrue(errors.size() > 0);
    IStatus status = (IStatus) errors.get(errors.size() - 1);
    return status;
  }

  private boolean isLastError(IStatus status) {
    return (errors.size() > 0 && getLastError() == status);
  }

  protected void handleErrors(IStatus[] errors) throws SVNException {
    if (errors.length == 0) return;
    if (errors.length == 1 && statusCount == 1) {
      throw new SVNException(errors[0]);
    }
    MultiStatus result =
        new MultiStatus(SVNUIPlugin.ID, 0, getErrorMessage(errors, statusCount), null);
    for (int i = 0; i < errors.length; i++) {
      IStatus s = errors[i];
      if (s.isMultiStatus()) {
        result.add(new SVNStatus(s.getSeverity(), s.getMessage(), s.getException()));
        result.addAll(s);
      } else {
        result.add(s);
      }
    }
    throw new SVNException(result);
  }

  protected String getErrorMessage(IStatus[] failures, int totalOperations) {
    return Policy.bind(
        "SVNOperation.0",
        String.valueOf(failures.length),
        String.valueOf(totalOperations)); // $NON-NLS-1$
  }

  /**
   * This method prompts the user to overwrite an existing resource. It uses the <code>
   * involvesMultipleResources</code> to determine what buttons to show.
   *
   * @param project
   * @return
   */
  protected boolean promptToOverwrite(final String title, final String msg) {
    if (!confirmOverwrite) {
      return true;
    }
    final String buttons[];
    if (involvesMultipleResources()) {
      buttons =
          new String[] {
            IDialogConstants.YES_LABEL,
            IDialogConstants.YES_TO_ALL_LABEL,
            IDialogConstants.NO_LABEL,
            IDialogConstants.CANCEL_LABEL
          };
    } else {
      buttons = new String[] {IDialogConstants.OK_LABEL, IDialogConstants.CANCEL_LABEL};
    }
    final Shell displayShell = getShell();
    if (displayShell == null) {
      // We couldn't get a shell (perhaps due to shutdown)
      return false;
    }

    // run in syncExec because callback is from an operation,
    // which is probably not running in the UI thread.
    final int[] code = new int[] {0};
    displayShell
        .getDisplay()
        .syncExec(
            new Runnable() {
              public void run() {
                MessageDialog dialog =
                    new MessageDialog(
                        displayShell, title, null, msg, MessageDialog.QUESTION, buttons, 0);
                dialog.open();
                code[0] = dialog.getReturnCode();
              }
            });
    if (involvesMultipleResources()) {
      switch (code[0]) {
        case 0: // Yes
          return true;
        case 1: // Yes to all
          confirmOverwrite = false;
          return true;
        case 2: // No
          return false;
        case 3: // Cancel
        default:
          throw new OperationCanceledException();
      }
    } else {
      return code[0] == 0;
    }
  }

  /**
   * This method is used by <code>promptToOverwrite</code> to determine which buttons to show in the
   * prompter.
   *
   * @return
   */
  protected boolean involvesMultipleResources() {
    return involvesMultipleResources;
  }

  public void setInvolvesMultipleResources(boolean b) {
    involvesMultipleResources = b;
  }

  /**
   * Return the string that is to be used as the task name for the operation
   *
   * @param remoteFolders
   * @return
   */
  protected abstract String getTaskName();

  /**
   * Return true if any of the accumulated status have a severity of ERROR
   *
   * @return
   */
  protected boolean errorsOccurred() {
    for (Iterator iter = errors.iterator(); iter.hasNext(); ) {
      IStatus status = (IStatus) iter.next();
      if (status.getSeverity() == IStatus.ERROR) return true;
    }
    return false;
  }

  /* (non-Javadoc)
   * @see org.eclipse.team.internal.ui.actions.TeamOperation#getShell()
   */
  protected Shell getShell() {
    // Use the shell assigned to the operation if possible
    if (shell != null && !shell.isDisposed()) {
      return shell;
    }
    return super.getShell();
  }

  /**
   * Set the shell to be used by the operation. This only needs to be done if the operation does not
   * have a workbench part. For example, if the operation is being run in a wizard.
   *
   * @param shell The shell to set.
   */
  public void setShell(Shell shell) {
    this.shell = shell;
  }

  /* (non-Javadoc)
   * @see org.eclipse.team.ui.TeamOperation#canRunAsJob()
   */
  protected boolean canRunAsJob() {
    // Put SVN jobs in the background by default.
    return true;
  }

  public void showCancelledMessage() {
    Display.getDefault()
        .asyncExec(
            new Runnable() {
              public void run() {
                MessageDialog.openInformation(
                    getShell(),
                    getJobName(),
                    Policy.bind("SVNOperation.operationCancelled")); // $NON-NLS-1$
              }
            });
  }

  //	protected ISchedulingRule getSchedulingRule() {
  //		// XXX IGORF consider locking affected projects only
  //		return ResourcesPlugin.getWorkspace().getRoot();
  //	}
}
